﻿Imports System.Windows.SystemParameters

Public Class RegexTextBoxAdorner
    Inherits Adorner

#Region "Events"
    ' Insert Event Declarations Here
#End Region

#Region "Properties"
#Region "Constants"
    ' Insert Constant Declarations Here
#End Region


    Public Shared ReadOnly IsRegexAdornerEnabledProperty As DependencyProperty =
        DependencyProperty.RegisterAttached(
            name:="IsRegexAdornerEnabled",
            propertyType:=GetType(Boolean),
            ownerType:=GetType(RegexTextBoxAdorner),
            defaultMetadata:=Nothing
        )
    Public Shared Function GetIsRegexAdornerEnabled(target As TextBox) As Boolean
        Return CBool(target.GetValue(IsRegexAdornerEnabledProperty))
    End Function
    Public Shared Sub SetIsRegexAdornerEnabled(target As TextBox, value As Boolean)
        target.SetValue(IsRegexAdornerEnabledProperty, value)

        If value Then
            Dim l_adornerLayer = AdornerLayer.GetAdornerLayer(target)
            l_adornerLayer.Add(New RegexTextBoxAdorner(target))
        Else
            Dim l_adornerLayer = AdornerLayer.GetAdornerLayer(target)
            For Each l_adorner In l_adornerLayer.GetAdorners(target).Where(Function(a) TypeOf a Is RegexTextBoxAdorner)
                l_adornerLayer.Remove(l_adorner)
            Next
        End If
    End Sub

    Public Shared ReadOnly SelectionStartProperty As DependencyProperty =
        DependencyProperty.RegisterAttached(
            name:="SelectionStart",
            propertyType:=GetType(Integer),
            ownerType:=GetType(RegexTextBoxAdorner),
            defaultMetadata:=New FrameworkPropertyMetadata(
                New PropertyChangedCallback(
                    Sub(d As DependencyObject, e As DependencyPropertyChangedEventArgs)

                        Dim l_textbox = TryCast(d, TextBox)
                        If l_textbox IsNot Nothing Then

                            Dim l_adorners = GetAdorners(l_textbox)
                            If l_adorners IsNot Nothing Then

                                For Each l_adorner In l_adorners
                                    l_adorner.InvalidateVisual()
                                Next

                            End If

                        End If

                    End Sub
                )
            )
        )
    Public Shared Function GetSelectionStart(ByVal element As TextBox) As Integer
        If element Is Nothing Then Throw New ArgumentNullException("element")
        Return element.GetValue(SelectionStartProperty)
    End Function
    Public Shared Sub SetSelectionStart(ByVal element As TextBox, ByVal value As Integer)
        If element Is Nothing Then Throw New ArgumentNullException("element")
        element.SetValue(SelectionStartProperty, value)
    End Sub

    Public Shared ReadOnly SelectionLengthProperty As DependencyProperty =
        DependencyProperty.RegisterAttached(
            "SelectionLength",
            GetType(Integer),
            GetType(RegexTextBoxAdorner),
            defaultMetadata:=New FrameworkPropertyMetadata(
                New PropertyChangedCallback(
                    Sub(d As DependencyObject, e As DependencyPropertyChangedEventArgs)

                        Dim l_textbox = TryCast(d, TextBox)
                        If l_textbox IsNot Nothing Then

                            Dim l_adorners = GetAdorners(l_textbox)
                            If l_adorners IsNot Nothing Then

                                For Each l_adorner In l_adorners
                                    l_adorner.InvalidateVisual()
                                Next

                            End If

                        End If

                    End Sub
                )
            )
        )
    Public Shared Function GetSelectionLength(ByVal element As TextBox) As Integer
        If element Is Nothing Then Throw New ArgumentNullException("element")
        Return element.GetValue(SelectionLengthProperty)
    End Function

    Public Shared Sub SetSelectionLength(ByVal element As TextBox, ByVal value As Integer)
        If element Is Nothing Then Throw New ArgumentNullException("element")
        element.SetValue(SelectionLengthProperty, value)
    End Sub

#End Region

#Region "Constructors"
    Public Sub New(adornerElement As TextBox)
        MyBase.New(adornerElement)
    End Sub
#End Region

#Region "Methods"
    Private Shared Function GetAdorners(target As TextBox) As IEnumerable(Of RegexTextBoxAdorner)
        If target IsNot Nothing Then
            Dim l_adornerLayer = AdornerLayer.GetAdornerLayer(target)
            If l_adornerLayer IsNot Nothing Then
                Dim l_adorners = l_adornerLayer.GetAdorners(target)
                If GetIsRegexAdornerEnabled(target) Then
                    If l_adorners IsNot Nothing AndAlso l_adorners.Any() Then
                        Return l_adornerLayer.GetAdorners(target).Where(Function(a) TypeOf a Is RegexTextBoxAdorner).Cast(Of RegexTextBoxAdorner)()
                    Else
                        Dim l_adorner = New RegexTextBoxAdorner(target)
                        l_adorner.IsHitTestVisible = False
                        target.AddHandler(
                            ScrollViewer.ScrollChangedEvent,
                            New ScrollChangedEventHandler(
                                Sub(sender As DependencyObject, e As ScrollChangedEventArgs)
                                    l_adorner.InvalidateVisual()
                                End Sub),
                            True
                        )
                        l_adornerLayer.Add(l_adorner)
                        Return New RegexTextBoxAdorner() {l_adorner}
                    End If
                Else
                    If l_adorners IsNot Nothing AndAlso l_adorners.Any() Then
                        For Each l_adorner In l_adorners
                            If TypeOf l_adorner Is RegexTextBoxAdorner Then l_adornerLayer.Remove(l_adorner)
                        Next
                        Return Nothing
                    End If
                End If
            End If
        End If

        Return Nothing
    End Function

    Public Sub SetSelection(start As Integer, length As Integer)
        SetSelectionStart(AdornedElement, start)
        SetSelectionLength(AdornedElement, length)
        Me.InvalidateVisual()
    End Sub

    Protected Overrides Sub OnRender(drawingContext As System.Windows.Media.DrawingContext)
        Dim l_textbox = DirectCast(AdornedElement, TextBox)

        drawingContext.PushClip(
            New RectangleGeometry(
                New Rect(
                    0,
                    0,
                    l_textbox.ActualWidth - (VerticalScrollBarWidth + l_textbox.Padding.Left + l_textbox.Padding.Right),
                    l_textbox.ActualHeight - (HorizontalScrollBarHeight + l_textbox.Padding.Top + l_textbox.Padding.Bottom))))

        Dim l_selectionStart = GetSelectionStart(AdornedElement)
        Dim l_selectionEnd = l_selectionStart + GetSelectionLength(AdornedElement)

        Dim l_firstLine = l_textbox.GetLineIndexFromCharacterIndex(l_selectionStart)
        Dim l_lastLine = l_textbox.GetLineIndexFromCharacterIndex(l_selectionEnd)

        ' If the selection has changed since the last
        Static l_previousSelection As Tuple(Of Integer, Integer) = New Tuple(Of Integer, Integer)(-1, -1)
        If l_previousSelection.Item1 <> l_selectionStart OrElse l_previousSelection.Item2 <> l_selectionEnd Then
            l_previousSelection = New Tuple(Of Integer, Integer)(l_selectionStart, l_selectionEnd)
            l_textbox.ScrollToLine(l_firstLine)
        End If

        'The highlighted text is spread across several lines
        For l_lineIndex = l_firstLine To l_lastLine
            Dim l_startIndex = If(l_lineIndex = l_firstLine, l_selectionStart, l_textbox.GetCharacterIndexFromLineIndex(l_lineIndex))
            Dim l_endIndex = If(l_lineIndex = l_lastLine, l_selectionEnd, l_textbox.GetCharacterIndexFromLineIndex(l_lineIndex) + l_textbox.GetLineLength(l_lineIndex))

            Dim l_topLeft = l_textbox.GetRectFromCharacterIndex(l_startIndex)
            Dim l_bottomRight = If(l_endIndex - l_startIndex > 0, l_textbox.GetRectFromCharacterIndex(l_endIndex - 1, True), l_topLeft)

            If l_lineIndex = l_firstLine Then
                Dim l_startMarkerPoint = New Point(l_topLeft.X, l_topLeft.Y - 2)
                drawingContext.DrawEllipse(Brushes.Green, Nothing, l_startMarkerPoint, 2, 2)
                drawingContext.DrawLine(New Pen(Brushes.Green, 1), l_startMarkerPoint, Point.Add(l_startMarkerPoint, New Vector(0, l_topLeft.Height + 2)))
            End If

            If l_lineIndex = l_lastLine AndAlso (l_lastLine > l_firstLine OrElse l_endIndex > l_startIndex) Then
                Dim l_endMarkerPoint = New Point(CInt(l_bottomRight.X), l_bottomRight.Y + l_bottomRight.Height + 2)
                drawingContext.DrawEllipse(Brushes.Red, Nothing, l_endMarkerPoint, 2, 2)
                drawingContext.DrawLine(New Pen(Brushes.Red, 1), l_endMarkerPoint, Point.Subtract(l_endMarkerPoint, New Vector(0, l_topLeft.Height + 2)))
            End If

            If l_bottomRight <> l_topLeft Then

                Dim l_span =
                    New Rect(
                        CInt(l_topLeft.X),
                        CInt(l_topLeft.Y),
                        CInt(Math.Max(l_bottomRight.X - l_topLeft.X, 0)),
                        CInt(Math.Max(l_bottomRight.Y - l_topLeft.Y, 0) + l_bottomRight.Height)
                    )

                drawingContext.DrawRectangle(New SolidColorBrush(Color.FromArgb(&H20, &H0, &H0, &HFF)), Nothing, l_span)
            End If
        Next


        MyBase.OnRender(drawingContext)
    End Sub

#Region "Event Raiser Methods"
    ' Insert Method Declarations which Raise an Event Here
#End Region

#Region "Event Handlers"
    ' Insert Method Declarations which Handle an Event Here
#End Region
#End Region

End Class
